% Licensed under the Apache License, Version 2.0 (the "License"); you may not
% use this file except in compliance with the License. You may obtain a copy of
% the License at
%
%   http://www.apache.org/licenses/LICENSE-2.0
%
% Unless required by applicable law or agreed to in writing, software
% distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
% WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
% License for the specific language governing permissions and limitations under
% the License.

-module(test_util).

-export([init_code_path/0]).
-export([source_file/1, build_file/1]).
-export([revtree_generate/4, revtree_get_revs/1, random_rev/0]).

-export([start_couch/0, start_couch/1, start_couch/2, stop_couch/0, stop_couch/1]).
-export([start_config/1, stop_config/1]).
-export([start_applications/1, stop_applications/1]).

-export([stop_sync/1, stop_sync/2, stop_sync/3]).

-export([stop_sync_throw/2, stop_sync_throw/3, stop_sync_throw/4]).

-export([with_process_restart/1, with_process_restart/2, with_process_restart/3]).
-export([wait_process/1, wait_process/2]).
-export([wait/1, wait/2, wait/3]).
-export([wait_value/2, wait_other_value/2]).
-export([with_processes_restart/2, with_processes_restart/4]).
-export([with_couch_server_restart/1]).

-export([start/1, start/2, start/3, stop/1]).

-export([fake_db/1]).

-export([shuffle/1]).

-export([as_selector/1]).

-include_lib("couch/include/couch_eunit.hrl").
-include_lib("couch/include/couch_db.hrl").
-include("couch_db_int.hrl").
-include("couch_bt_engine.hrl").

-record(test_context, {mocked = [], started = [], module}).

-define(DEFAULT_APPS, [inets, ibrowse, ssl, config, couch_epi, couch_event, couch]).

srcdir() ->
    code:priv_dir(couch) ++ "/../../".

builddir() ->
    code:priv_dir(couch) ++ "/../../../".

init_code_path() ->
    Paths = [
        "couchdb",
        "jiffy",
        "ibrowse",
        "mochiweb",
        "snappy"
    ],
    lists:foreach(
        fun(Name) ->
            code:add_patha(filename:join([builddir(), "src", Name]))
        end,
        Paths
    ).

source_file(Name) ->
    filename:join([srcdir(), Name]).

build_file(Name) ->
    filename:join([builddir(), Name]).

start_couch() ->
    start_couch(?CONFIG_CHAIN, []).

start_couch(ExtraApps) ->
    start_couch(?CONFIG_CHAIN, ExtraApps).

start_couch(IniFiles, ExtraApps) ->
    load_applications_with_stats(),
    ok = application:set_env(config, ini_files, IniFiles),
    Apps = start_applications(?DEFAULT_APPS ++ ExtraApps),
    ok = config:delete("compactions", "_default", false),
    #test_context{started = Apps}.

stop_couch() ->
    ok = stop_applications(?DEFAULT_APPS).

stop_couch(#test_context{started = Apps}) ->
    stop_applications(Apps);
stop_couch(_) ->
    stop_couch().

with_couch_server_restart(Fun) ->
    Servers = couch_server:names(),
    test_util:with_processes_restart(Servers, Fun).

start_applications(Apps) ->
    StartOrder = calculate_start_order(Apps),
    start_applications(StartOrder, []).

start_applications([], Acc) ->
    lists:reverse(Acc);
start_applications([App | Apps], Acc) when App == kernel; App == stdlib ->
    start_applications(Apps, Acc);
start_applications([App | Apps], Acc) ->
    case application:start(App) of
        {error, {already_started, crypto}} ->
            start_applications(Apps, [crypto | Acc]);
        {error, {already_started, App}} ->
            io:format(standard_error, "Application ~s was left running!~n", [App]),
            application:stop(App),
            start_applications([App | Apps], Acc);
        {error, Reason} ->
            io:format(standard_error, "Cannot start application '~s', reason ~p~n", [App, Reason]),
            throw({error, {cannot_start, App, Reason}});
        ok ->
            start_applications(Apps, [App | Acc])
    end.

stop_applications(Apps) ->
    [application:stop(App) || App <- lists:reverse(Apps)],
    ok.

start_config(Chain) ->
    case config:start_link(Chain) of
        {ok, Pid} ->
            {ok, Pid};
        {error, {already_started, OldPid}} ->
            ok = stop_config(OldPid),
            start_config(Chain)
    end.

stop_config(Pid) ->
    Timeout = 1000,
    case stop_sync(Pid, fun() -> config:stop() end, Timeout) of
        timeout ->
            throw({timeout_error, config_stop});
        _Else ->
            ok
    end.

stop_sync(Name) ->
    stop_sync(Name, shutdown).
stop_sync(Name, Reason) ->
    stop_sync(Name, Reason, 5000).

stop_sync(Name, Reason, Timeout) when is_atom(Name) ->
    stop_sync(whereis(Name), Reason, Timeout);
stop_sync(Pid, Reason, Timeout) when is_atom(Reason) and is_pid(Pid) ->
    stop_sync(Pid, fun() -> exit(Pid, Reason) end, Timeout);
stop_sync(Pid, Fun, Timeout) when is_function(Fun) and is_pid(Pid) ->
    MRef = erlang:monitor(process, Pid),
    try
        begin
            catch unlink(Pid),
            Res = (catch Fun()),
            receive
                {'DOWN', MRef, _, _, _} ->
                    Res
            after Timeout ->
                timeout
            end
        end
    after
        erlang:demonitor(MRef, [flush])
    end;
stop_sync(_, _, _) ->
    error(badarg).

stop_sync_throw(Name, Error) ->
    stop_sync_throw(Name, shutdown, Error).
stop_sync_throw(Name, Reason, Error) ->
    stop_sync_throw(Name, Reason, Error, 5000).

stop_sync_throw(Pid, Fun, Error, Timeout) ->
    case stop_sync(Pid, Fun, Timeout) of
        timeout ->
            throw(Error);
        Else ->
            Else
    end.

with_process_restart(Name) ->
    {Pid, true} = with_process_restart(
        Name, fun() -> exit(whereis(Name), shutdown) end
    ),
    Pid.

with_process_restart(Name, Fun) ->
    with_process_restart(Name, Fun, 5000).

with_process_restart(Name, Fun, Timeout) ->
    Res = stop_sync(Name, Fun),
    case wait_process(Name, Timeout) of
        timeout ->
            timeout;
        Pid ->
            {Pid, Res}
    end.

wait_process(Name) ->
    wait_process(Name, 5000).
wait_process(Name, Timeout) ->
    wait(
        fun() ->
            case whereis(Name) of
                undefined ->
                    wait;
                Pid ->
                    Pid
            end
        end,
        Timeout
    ).

wait(Fun) ->
    wait(Fun, 5000, 50).

wait(Fun, Timeout) ->
    wait(Fun, Timeout, 50).

wait(Fun, Timeout, Delay) ->
    Now = now_us(),
    wait(Fun, Timeout * 1000, Delay, Now, Now).

wait(_Fun, Timeout, _Delay, Started, Prev) when Prev - Started > Timeout ->
    timeout;
wait(Fun, Timeout, Delay, Started, _Prev) ->
    case Fun() of
        wait ->
            ok = timer:sleep(Delay),
            wait(Fun, Timeout, Delay, Started, now_us());
        Else ->
            Else
    end.

wait_value(Fun, Value) ->
    wait(fun() ->
        case Fun() of
            Value -> Value;
            _ -> wait
        end
    end).

wait_other_value(Fun, Value) ->
    wait(fun() ->
        case Fun() of
            Value -> wait;
            Other -> Other
        end
    end).

with_processes_restart(Processes, Fun) ->
    with_processes_restart(Processes, Fun, 5000, 50).

with_processes_restart(Names, Fun, Timeout, Delay) ->
    Processes = lists:foldl(
        fun(Name, Acc) ->
            [{Name, whereis(Name)} | Acc]
        end,
        [],
        Names
    ),
    [catch unlink(Pid) || {_, Pid} <- Processes],
    Res = (catch Fun()),
    {wait_start(Processes, Timeout, Delay), Res}.

wait_start(Processses, TimeoutInSec, Delay) ->
    Now = now_us(),
    wait_start(Processses, TimeoutInSec * 1000, Delay, Now, Now, #{}).

wait_start(_, Timeout, _Delay, Started, Prev, _) when Prev - Started > Timeout ->
    timeout;
wait_start([], _Timeout, _Delay, _Started, _Prev, Res) ->
    Res;
wait_start([{Name, Pid} | Rest] = Processes, Timeout, Delay, Started, _Prev, Res) ->
    case whereis(Name) of
        NewPid when is_pid(NewPid) andalso NewPid =/= Pid ->
            wait_start(Rest, Timeout, Delay, Started, now_us(), maps:put(Name, NewPid, Res));
        _ ->
            ok = timer:sleep(Delay),
            wait_start(Processes, Timeout, Delay, Started, now_us(), Res)
    end.

start(Module) ->
    start(Module, [], []).

start(Module, ExtraApps) ->
    start(Module, ExtraApps, []).

start(Module, ExtraApps, Options) ->
    Apps = start_applications([config, couch_log, ioq, couch_epi | ExtraApps]),
    ToMock = [config, couch_stats] -- proplists:get_value(dont_mock, Options, []),
    mock(ToMock),
    #test_context{module = Module, mocked = ToMock, started = Apps}.

stop(#test_context{mocked = Mocked, started = Apps}) ->
    meck:unload(Mocked),
    stop_applications(Apps).

fake_db(Fields0) ->
    {ok, Db, Fields} = maybe_set_engine(Fields0),
    Indexes = lists:zip(
        record_info(fields, db),
        lists:seq(2, record_info(size, db))
    ),
    lists:foldl(
        fun({FieldName, Value}, Acc) ->
            Idx = couch_util:get_value(FieldName, Indexes),
            setelement(Idx, Acc, Value)
        end,
        Db,
        Fields
    ).

maybe_set_engine(Fields0) ->
    case lists:member(engine, Fields0) of
        true ->
            {ok, #db{}, Fields0};
        false ->
            {ok, Header, Fields} = get_engine_header(Fields0),
            Db = #db{engine = {couch_bt_engine, #st{header = Header}}},
            {ok, Db, Fields}
    end.

get_engine_header(Fields) ->
    Keys = [
        disk_version,
        update_seq,
        unused,
        id_tree_state,
        seq_tree_state,
        local_tree_state,
        purge_seq,
        purged_docs,
        security_ptr,
        revs_limit,
        uuid,
        epochs,
        compacted_seq
    ],
    {HeadFields, RestFields} = lists:partition(
        fun({K, _}) -> lists:member(K, Keys) end, Fields
    ),
    Header0 = couch_bt_engine_header:new(),
    Header = couch_bt_engine_header:set(Header0, HeadFields),
    {ok, Header, RestFields}.

now_us() ->
    {MegaSecs, Secs, MicroSecs} = os:timestamp(),
    (MegaSecs * 1000000 + Secs) * 1000000 + MicroSecs.

mock(Modules) when is_list(Modules) ->
    [mock(Module) || Module <- Modules];
mock(config) ->
    meck:new(config, [passthrough]),
    meck:expect(config, get, fun(_, _) -> undefined end),
    meck:expect(config, get, fun(_, _, Default) -> Default end),
    ok;
mock(couch_stats) ->
    meck:new(couch_stats, [passthrough]),
    meck:expect(couch_stats, increment_counter, fun(_) -> ok end),
    meck:expect(couch_stats, increment_counter, fun(_, _) -> ok end),
    meck:expect(couch_stats, decrement_counter, fun(_) -> ok end),
    meck:expect(couch_stats, decrement_counter, fun(_, _) -> ok end),
    meck:expect(couch_stats, update_histogram, fun(_, _) -> ok end),
    meck:expect(couch_stats, update_gauge, fun(_, _) -> ok end),
    ok.

load_applications_with_stats() ->
    Wildcard = filename:join([?BUILDDIR(), "src/*/priv/stats_descriptions.cfg"]),
    [application:load(stats_file_to_app(File)) || File <- filelib:wildcard(Wildcard)],
    ok.

stats_file_to_app(File) ->
    [_Desc, _Priv, App | _] = lists:reverse(filename:split(File)),
    erlang:list_to_atom(App).

calculate_start_order(Apps) ->
    AllApps = calculate_start_order(sort_apps(Apps), []),
    % AllApps may not be the same list as Apps if we
    % loaded any dependencies. We recurse here when
    % that changes so that our sort_apps function has
    % a global view of all applications to start.
    case lists:usort(AllApps) == lists:usort(Apps) of
        true -> AllApps;
        false -> calculate_start_order(AllApps)
    end.

calculate_start_order([], StartOrder) ->
    lists:reverse(StartOrder);
calculate_start_order([App | RestApps], StartOrder) ->
    NewStartOrder = load_app_deps(App, StartOrder),
    calculate_start_order(RestApps, NewStartOrder).

load_app_deps(App, StartOrder) ->
    case lists:member(App, StartOrder) of
        true ->
            StartOrder;
        false ->
            case application:load(App) of
                ok -> ok;
                {error, {already_loaded, App}} -> ok
            end,
            {ok, Apps} = application:get_key(App, applications),
            Deps =
                case App of
                    kernel -> Apps;
                    stdlib -> Apps;
                    _ -> lists:usort([kernel, stdlib | Apps])
                end,
            NewStartOrder = lists:foldl(
                fun(Dep, Acc) ->
                    load_app_deps(Dep, Acc)
                end,
                StartOrder,
                Deps
            ),
            [App | NewStartOrder]
    end.

sort_apps(Apps) ->
    Weighted = [weight_app(App) || App <- Apps],
    element(2, lists:unzip(lists:sort(Weighted))).

weight_app(couch_log) -> {0.0, couch_log};
weight_app(Else) -> {1.0, Else}.

% Generate random rev trees
%
% Args:
%   Depth : Max depth. This will be halfed every time we branch.
%
%   BranchChance : 0.0 to 1.0 chance of branching at each level.
%
%   WideBranches: 1/4 of the time when branching happens it will create
%                 wide branches, this specifies the width of those branches.
%
% Example usage: revtree_generate(25, 0.25, 10, os:timestamp())

revtree_generate(Depth, BranchChance, WideBranches, Seed) ->
    rand:seed(exrop, Seed),
    Rev = random_rev(),
    {Seed, [{1, revnode(Rev, Depth, BranchChance, WideBranches)}]}.

% Get all the revisions in the tree as a sorted [{Pos, Rev} ...] list
%
revtree_get_revs([{Pos, {_, _, _} = Node}]) when is_integer(Pos) ->
    lists:sort(maps:keys(revs1(Pos, Node))).

revnode(Rev, Depth, _, _) when Depth =< 0 ->
    {Rev, x, []};
revnode(Rev, Depth, BranchChance, WideBranches) ->
    Choice = rand:uniform(),
    {Revs, Depth1} =
        if
            Choice < BranchChance / 4 ->
                {childrev(WideBranches), trunc(Depth / 2)};
            Choice < BranchChance ->
                {childrev(2), trunc(Depth / 2)};
            true ->
                {childrev(1), Depth - 1}
        end,
    {Rev, x, [revnode(R, Depth1, BranchChance, WideBranches) || R <- Revs]}.

childrev(N) ->
    lists:sort([random_rev() || _ <- lists:seq(1, N)]).

revs1(Pos, {Rev, _Val, []}) ->
    #{{Pos, Rev} => true};
revs1(Pos, {Rev, _Val, Nodes}) ->
    lists:foldl(
        fun(N, Acc) ->
            maps:merge(Acc, revs1(Pos + 1, N))
        end,
        #{{Pos, Rev} => true},
        Nodes
    ).

random_rev() ->
    couch_util:to_hex_bin(crypto:strong_rand_bytes(16)).

shuffle(List) ->
    Paired = [{couch_rand:uniform(), I} || I <- List],
    Sorted = lists:sort(Paired),
    [I || {_, I} <- Sorted].

%% Create a valid Mango selector from an Erlang map.
as_selector(Map) ->
    mango_selector:normalize(jiffy:decode(jiffy:encode(Map))).
